package cxm.proj.xtalk.smack;

import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.SmackAndroid;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smackx.ping.PingManager;

import android.content.Intent;
import android.os.AsyncTask;
import android.support.v4.content.LocalBroadcastManager;
import cxm.lib.sys.AsyncStartMachine;
import cxm.lib.util.MyApplication;
import cxm.lib.util.MyLog;

public class Connector implements ConnectionListener {
	private enum ConnectState {
		Disconnected,
		Connecting,
		Connected,
		Disconnecting
	}
	
	@SuppressWarnings("serial")
	public class ConnectException extends Exception {
		public ConnectException(String msg) {
			super("Connect fail: " + msg);
		}
	}
	
	public static final String CONNECTION_EVENT = "cxm.proj.xtalk.connection_event";
	public static final int CONNECTION_CLOSED_ERROR = 1;
	public static final int CONNECTION_ESTABLISHED = 2;
	
	private String ipaddr = null;
	private String serviceName = null;
	private int port = 5222;
	private ConnectionConfiguration connectionConfiguration = null;
	private XMPPConnection mXmppConnection = null;
	private ConnectorStatus connectState = null;
	private KeepAlivePingManager pingManager = null;
	// private PingManager pingManager = null;
	// private PacketListener  packetListener = null;

	public Connector(String ipAddress, String serviceName) {
		this.ipaddr = ipAddress;
		this.serviceName = serviceName;
		this.connectState = new DisconnectStatus();
		
		// init asmack
		SmackAndroid.init(MyApplication.context);
		// org.jivesoftware.smack.SmackConfiguration.setDefaultPingInterval(60);
	}
	
	// Async process, call taskResultHandle in UI thread when complete
	public void connect(ConnectTaskResultHandle taskResultHandle,
			Object taskResultHandleTag) throws ConnectException {
		this.handleAsyncTask(new ConnectTaskHandle() {
			@Override
			public int taskHandle(Connector connector) {
				try {
					Connector.this.connect();
					return 0;
				} catch (Exception e) {
					MyLog.Log(MyLog.Level.Error, "Cannot connect: " + e);
					return -1;
				}
			}
		}, taskResultHandle, taskResultHandleTag);
	}
	
	// sync connect
	public void connect() throws ConnectException {
		this.connectState.connect();
	}
	
	// Async process, call taskResultHandle when complete
	public void disconnect(ConnectTaskResultHandle taskResultHandle,
			Object taskResultHandleTag) throws ConnectException {
		this.handleAsyncTask(new ConnectTaskHandle() {
			public int taskHandle(Connector connector) {
				try {
					Connector.this.connectState.disconnect();
					return 0;
				} catch (Exception e) {
					MyLog.Log(MyLog.Level.Error, "Cannot disconnect: " + e);
					return -1;
				}
			}
		}, taskResultHandle, taskResultHandleTag);
	}
	
	// sync disconnect
	public void disconnect() throws ConnectException {
		this.connectState.disconnect();
	}
	
	// execute taskHandle in an async task
	// then call taskResultHandle with result
	public void handleAsyncTask(ConnectTaskHandle taskHandle,
			ConnectTaskResultHandle taskResultHandle, Object taskResultHandleTag) {
		if (null == taskHandle)
			throw new IllegalArgumentException("taskHandle null");
		
		ConnectTask connectTask = new ConnectTask(taskHandle,
				taskResultHandle, taskResultHandleTag);
		connectTask.execute();
	}
	
	public boolean isConnected() {
		return this.connectState.getState() == ConnectState.Connected;
	}
	
	public XMPPConnection getConnection() {
		return this.mXmppConnection;
	}
	
	public KeepAlivePingManager getPingManager() {
		return this.pingManager;
	}
	
	public ConnectState getState() {
		return this.connectState.getState();
	}
	
	private void setStatus(ConnectorStatus status) {
		this.connectState = status;
	}
	
	private XMPPConnection createConnection () {
		// construct connection
		connectionConfiguration = new ConnectionConfiguration(
        		ipaddr, port, serviceName);
		connectionConfiguration.setTruststoreType("AndroidCAStore");
		connectionConfiguration.setTruststorePassword(null);
		connectionConfiguration.setTruststorePath(null);
		connectionConfiguration.setReconnectionAllowed(false);
		connectionConfiguration.setSendPresence(true);
		connectionConfiguration.setRosterLoadedAtLogin(false);
		connectionConfiguration.setExpiredCertificatesCheckEnabled(false);
		connectionConfiguration.setNotMatchingDomainCheckEnabled(false);
		connectionConfiguration.setSelfSignedCertificateEnabled(true);
		connectionConfiguration.setVerifyChainEnabled(false);
		connectionConfiguration.setVerifyRootCAEnabled(false);

		// connectionConfiguration.setSASLAuthenticationEnabled(saslEnabled);
		// connectionConfiguration.setSecurityMode(tlsMode.getSecurityMode());
		// connectionConfiguration.setCompressionEnabled(compression);
		XMPPConnection xmppConnection = new XMPPConnection(connectionConfiguration);
		xmppConnection.addConnectionListener(this);
		
		return xmppConnection;
	}
	
	private abstract class ConnectorStatus {
		ConnectState status;
		public ConnectorStatus(ConnectState status) {
			this.status = status;
		}
		
		public ConnectState getState() {
			return this.status;
		}

		public abstract void connect() throws ConnectException;
		public abstract void disconnect() throws ConnectException;
	}
	
	private class DisconnectStatus extends ConnectorStatus {
		public DisconnectStatus() {
			super(ConnectState.Disconnected);
		}
		
		public void connect() throws ConnectException {
			// set status
			Connector.this.setStatus(new ConnectingStatus());
			// connect
			Connector.this.mXmppConnection = createConnection();
			try {
				Connector.this.mXmppConnection.connect();
				// create ping manager to response server ping
				Connector.this.pingManager = new KeepAlivePingManager(
						Connector.this.mXmppConnection);
				Connector.this.pingManager.start();

				// set status
				Connector.this.setStatus(new ConnectedStatus());

				MyLog.Log(MyLog.Level.Info, "Send broadcast for CONNECT ESTABLISH");
				// send broadcast
				Intent intent = new Intent(CONNECTION_EVENT);
				intent.putExtra("type", CONNECTION_ESTABLISHED);
				LocalBroadcastManager.getInstance(MyApplication.context).sendBroadcast(intent);
			} catch (Exception e) {
				// revert status
				Connector.this.pingManager.stop();
				Connector.this.pingManager = null;
				Connector.this.mXmppConnection.disconnect();
				Connector.this.mXmppConnection = null;
				Connector.this.setStatus(this);
				
				throw new ConnectException(e.toString());
			}
		}
		
		public void disconnect() throws ConnectException {
			// return directly
			return;
		}
	}
	
	private class ConnectingStatus extends ConnectorStatus {
		public ConnectingStatus() {
			super(ConnectState.Connecting);
		}

		@Override
		public void connect() throws ConnectException {
			throw new ConnectException("Connecting");
		}

		@Override
		public void disconnect() throws ConnectException {
			throw new ConnectException("Connecting");
		}
	}
	
	private class ConnectedStatus extends ConnectorStatus {
		public ConnectedStatus() {
			super(ConnectState.Connected);
		}

		@Override
		public void connect() throws ConnectException {
			throw new ConnectException("Already connected");
		}

		@Override
		public void disconnect() throws ConnectException {
			// set status first
			Connector.this.setStatus(new DisconnectingStatus());

			pingManager.stop();
			pingManager = null;
			mXmppConnection.disconnect();
			mXmppConnection = null;
			
			// disconnected
			Connector.this.setStatus(new DisconnectStatus());
		}
	}

	private class DisconnectingStatus extends ConnectorStatus {
		public DisconnectingStatus() {
			super(ConnectState.Disconnecting);
		}

		@Override
		public void connect() throws ConnectException {
			throw new ConnectException("Disconnecting");
		}

		@Override
		public void disconnect() throws ConnectException {
			// return directly to wait a while
			return;
		}
	}
	
	private class ConnectTask extends AsyncTask<Void, Integer, Void> {
		private ConnectTaskHandle taskHandle = null;
		private ConnectTaskResultHandle taskResultHandle = null;
		private Object taskResultHandleTag = null;
		private int result = 0;
		
		public ConnectTask(ConnectTaskHandle taskHandle,
				ConnectTaskResultHandle taskResultHandle,
				Object taskResultHandleTag) {
			this.taskHandle = taskHandle;
			this.taskResultHandle = taskResultHandle;
			this.taskResultHandleTag = taskResultHandleTag;
		}
		
		@Override
		protected Void doInBackground(Void... params) {
			this.result = this.taskHandle.taskHandle(Connector.this);
			return null;
		}
		
		@Override
		protected void onPostExecute(Void result) {
			if (null != this.taskResultHandle)
				this.taskResultHandle.taskResultHandle(this.result,
						this.taskResultHandleTag);
			else
				MyLog.Log(MyLog.Level.Info, "Task result handle null");
	    }
	}
	
	private class KeepAlivePingManager extends AsyncStartMachine {
		private PingManager pingManager = null;
		private int pingInterval = 60 * 1000;
		private XMPPConnection xmpp = null;
		private int runInterval = 500;
		private int runCount = 0;

		public KeepAlivePingManager(XMPPConnection xmpp) {
			this.pingManager = PingManager.getInstanceFor(xmpp);
			this.xmpp = xmpp;
		}
		
		@SuppressWarnings("unused")
		public KeepAlivePingManager(XMPPConnection xmpp, int pingIntervalInMils) {
			this.pingManager = PingManager.getInstanceFor(xmpp);
			this.pingInterval = pingIntervalInMils;
		}

		@Override
		protected void onRunOnce() {
        	if (this.pingInterval / this.runInterval <= this.runCount) {
        		MyLog.Log(MyLog.Level.Debug, "Ping server");
        		try {
        			this.pingManager.pingMyServer();
        			Presence presence = new Presence(Presence.Type.available);
        			this.xmpp.sendPacket(presence);
        		} catch (Exception e) {
        			MyLog.Log(MyLog.Level.Error, "Ping server failed: " + e);
        		}
        		this.runCount = 0; // recercal
        	} else
				this.runCount++; // to reduce the run once interval
        	
        	try {
	        	Thread.sleep(this.runInterval);
        	} catch (Exception e) {
        		// ignore
        	}
		}
	}

	@Override
	public void connectionClosed() {
		MyLog.Log(MyLog.Level.Info, "Connection closed");
	}

	@Override
	public void connectionClosedOnError(Exception arg0) {
		MyLog.Log(MyLog.Level.Info, "Connection closed on error");
		
		// send broadcast
		Intent intent = new Intent(CONNECTION_EVENT);
		intent.putExtra("type", CONNECTION_CLOSED_ERROR);
		LocalBroadcastManager.getInstance(MyApplication.context).sendBroadcast(intent);
	}

	@Override
	public void reconnectingIn(int arg0) {
		MyLog.Log(MyLog.Level.Info, "Reconnecting");
	}

	@Override
	public void reconnectionFailed(Exception arg0) {
		MyLog.Log(MyLog.Level.Info, "Reconnection failed");
	}

	@Override
	public void reconnectionSuccessful() {
		MyLog.Log(MyLog.Level.Info, "Reconnection successsful");
	}
}
